22

前言

最近在学习vue2.0的源码,刚开始看其vdom源码,着实找不到方向,因为其在vdom的实现上还加
入了很多vue2.0本身的钩子,加大了阅读难度。于是看到第一行尤大说vue2.0的vdom是在snabbdom
的基础上改过来的,而snabbdom只有不到300sloc,那不妨先从snabbdom入手,熟悉其中的原理,
再配合vue2.0的vdom看,效果可能更好。

什么是virtual-dom

virtual-dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无
状态的组件,当组件状态发生更新时,然后触发virtual-dom数据的变化,然后通过virtual-dom
和真实DOM的比对,再对真实dom更新。

为什么是virtual-dom

我们知道,当我们希望实现一个具有复杂状态的界面时,如果我们在每个可能发生变化的组件上都绑定
事件,绑定字段数据,那么很快由于状态太多,我们需要维护的事件和字段将会越来越多,代码也会
越来越复杂,于是,我们想我们可不可以将视图和状态分开来,只要视图发生变化,对应状态也发生
变化,然后状态变化,我们再重绘整个视图就好了。这样的想法虽好,但是代价太高了,于是我们又
想,能不能只更新状态发生变化的视图?于是virtual-dom应运而生,状态变化先反馈到vdom上,
vdom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提升。

除此之外,从移植性上看,virtual-dom还对真实dom做了一次抽象,这意味着virtual-dom对应
的可以不是浏览器的dom,而是不同设备的组件,极大的方便了多平台的使用。

snabbdom目录结构

好了,说了这么多,我们先来看看snabbdom吧,我看的是这个版本的snabbdom
(心塞,typescript学的不深,看最新版的有点吃力,所以选了ts版本前的一个版本)。好了我们先
看看snabbdom的主要目录结构。

名称 类型 解释
dist 文件夹 里面包含了snabddom打包后的文件
examples 文件夹 里面包含了使用snabbdom的例子
helpers 文件夹 包含svg操作需要的工具
modules 文件夹 包含了对attribute,props,class,dataset,eventlistner,style,hero的操作
perf 文件夹 性能测试
test 文件夹 测试
h 文件 把状态转化为vnode
htmldomapi 文件 原生dom操作的抽象
is 文件 判断类型
snabbdom.bundle 文件 snabbdom本身依赖打包
snabbdom 文件 snabbdom 核心,包含diff,patch等操作
thunk 文件 snabbdom下的thunk功能实现
vnode 文件 构造vnode

snabbdom源码之旅

第一站 vnode

首先,我们从最简单的vnode开始入手,vnode实现的功能非常简单,就是讲输入的数据转化为vnode
对象的形式

    //VNode函数,用于将输入转化成VNode
    /**
     *
     * @param sel    选择器
     * @param data    绑定的数据
     * @param children    子节点数组
     * @param text    当前text节点内容
     * @param elm    对真实dom element的引用
     * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}}
     */
    module.exports = function ( sel, data, children, text, elm ) {
        var key = data === undefined ? undefined : data.key;
        return {
            sel: sel, data: data, children: children,
            text: text, elm: elm, key: key
        };
    };

vnode主要有5大属性:

  • sel 对应的是选择器,如'div','div#a','div#a.b.c'的形式

  • data 对应的是vnode绑定的数据,可以有以下类型:attribute、props、eventlistner、
    class、dataset、hook

  • children 子元素数组

  • text 文本,代表该节点中的文本内容

  • elm 里面存储着对对应的真实dom element的引用

  • key 用于不同vnode之间的比对

第二站 h

说完vnode,就到h了,h也是一个包装函数,主要是在vnode上再做一层包装,实现功能如下

  • 如果是svg,则为其添加命名空间

  • 将children中的text包装成vnode形式

    var VNode = require ( './vnode' );
    var is = require ( './is' );
    //添加命名空间(svg才需要)
    function addNS ( data, children, sel ) {
        data.ns = 'http://www.w3.org/2000/svg';
    //如果选择器
        if ( sel !== 'foreignObject' && children !== undefined ) {
            //递归为子节点添加命名空间
            for (var i = 0; i < children.length; ++i) {
                addNS ( children[ i ].data, children[ i ].children, children[ i ].sel );
            }
        }
    }
    //将VNode渲染为VDOM
    /**
     *
     * @param sel 选择器
     * @param b    数据
     * @param c    子节点
     * @returns {{sel, data, children, text, elm, key}}
     */
    module.exports = function h ( sel, b, c ) {
        var data = {}, children, text, i;
        //如果存在子节点
        if ( c !== undefined ) {
            //那么h的第二项就是data
            data = b;
            //如果c是数组,那么存在子element节点
            if ( is.array ( c ) ) {
                children = c;
            }
            //否则为子text节点
            else if ( is.primitive ( c ) ) {
                text = c;
            }
        }
        //如果c不存在,只存在b,那么说明需要渲染的vdom不存在data部分,只存在子节点部分
        else if ( b !== undefined ) {
            if ( is.array ( b ) ) {
                children = b;
            }
            else if ( is.primitive ( b ) ) {
                text = b;
            }
            else {
                data = b;
            }
        }
        if ( is.array ( children ) ) {
            for (i = 0; i < children.length; ++i) {
                //如果子节点数组中,存在节点是原始类型,说明该节点是text节点,因此我们将它渲染为一个只包含text的VNode
                if ( is.primitive ( children[ i ] ) ) children[ i ] = VNode ( undefined, undefined, undefined, children[ i ] );
            }
        }
        //如果是svg,需要为节点添加命名空间
        if ( sel[ 0 ] === 's' && sel[ 1 ] === 'v' && sel[ 2 ] === 'g' ) {
            addNS ( data, children, sel );
        }
        return VNode ( sel, data, children, text, undefined );
    };

第三站 htmldomapi

htmldomapi中提供了对原生dom操作的一层抽象,这里就不再阐述了

第四站 modules

modules中主要包含attributes,class,props,dataset,eventlistener,hero,style
这些模块,其中attributes,class,props,dataset,eventlistener,style这些模块是我们
日常所需要的,也是snabbdom.bundle默认注入的也是这几个,这里就详细介绍这几个模块

attributes

主要功能如下:

  • 从elm的属性中删除vnode中不存在的属性(包括那些boolean类属性,如果新vnode设置为false,同样删除)

  • 如果oldvnode与vnode用同名属性,则在elm上更新对应属性值

  • 如果vnode有新属性,则添加到elm中

  • 如果存在命名空间,则用setAttributeNS设置

    var NamespaceURIs = {
      "xlink": "http://www.w3.org/1999/xlink"
    };
    
    var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare",
                    "default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable",
                    "enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple",
                    "muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly",
                    "required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate",
                    "truespeed", "typemustmatch", "visible"];
    
    var booleanAttrsDict = Object.create(null);
    
    //创建属性字典,默认为true
    for(var i=0, len = booleanAttrs.length; i < len; i++) {
      booleanAttrsDict[booleanAttrs[i]] = true;
    }
    
    function updateAttrs(oldVnode, vnode) {
      var key, cur, old, elm = vnode.elm,
          oldAttrs = oldVnode.data.attrs, attrs = vnode.data.attrs, namespaceSplit;
    
    
      //如果旧节点和新节点都不包含属性,立刻返回
      if (!oldAttrs && !attrs) return;
      oldAttrs = oldAttrs || {};
      attrs = attrs || {};
    
      // update modified attributes, add new attributes
      //更新改变了的属性,添加新的属性
      for (key in attrs) {
        cur = attrs[key];
        old = oldAttrs[key];
        //如果旧的属性和新的属性不同
        if (old !== cur) {
        //如果是boolean类属性,当vnode设置为falsy value时,直接删除,而不是更新值
          if(!cur && booleanAttrsDict[key])
            elm.removeAttribute(key);
          else {
            //否则更新属性值或者添加属性
            //如果存在命名空间
            namespaceSplit = key.split(":");
            if(namespaceSplit.length > 1 && NamespaceURIs.hasOwnProperty(namespaceSplit[0]))
              elm.setAttributeNS(NamespaceURIs[namespaceSplit[0]], key, cur);
            else
              elm.setAttribute(key, cur);
          }
        }
      }
      //remove removed attributes
      // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
      // the other option is to remove all attributes with value == undefined
      //删除不在新节点属性中的旧节点的属性
      for (key in oldAttrs) {
        if (!(key in attrs)) {
          elm.removeAttribute(key);
        }
      }
    }
    
    module.exports = {create: updateAttrs, update: updateAttrs};

class

主要功能如下:

  • 从elm中删除vnode中不存在的或者值为false的类

  • 将vnode中新的class添加到elm上去

    function updateClass(oldVnode, vnode) {
      var cur, name, elm = vnode.elm,
          oldClass = oldVnode.data.class,
          klass = vnode.data.class;
      //如果旧节点和新节点都没有class,直接返回
      if (!oldClass && !klass) return;
      oldClass = oldClass || {};
      klass = klass || {};
      //从旧节点中删除新节点不存在的类
      for (name in oldClass) {
        if (!klass[name]) {
          elm.classList.remove(name);
        }
      }
      //如果新节点中对应旧节点的类设置为false,则删除该类,如果新设置为true,则添加该类
      for (name in klass) {
        cur = klass[name];
        if (cur !== oldClass[name]) {
          elm.classList[cur ? 'add' : 'remove'](name);
        }
      }
    }
    
    module.exports = {create: updateClass, update: updateClass};

dataset

主要功能如下:

  • 从elm中删除vnode不存在的属性集中的属性

  • 更新属性集中的属性值

    function updateDataset(oldVnode, vnode) {
      var elm = vnode.elm,
        oldDataset = oldVnode.data.dataset,
        dataset = vnode.data.dataset,
        key
    
      //如果新旧节点都没数据集,则直接返回
      if (!oldDataset && !dataset) return;
      oldDataset = oldDataset || {};
      dataset = dataset || {};
     //删除旧节点中在新节点不存在的数据集
      for (key in oldDataset) {
        if (!dataset[key]) {
          delete elm.dataset[key];
        }
      }
      //更新数据集
      for (key in dataset) {
        if (oldDataset[key] !== dataset[key]) {
          elm.dataset[key] = dataset[key];
        }
      }
    }
    
    module.exports = {create: updateDataset, update: updateDataset}

eventlistener

snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作,主要途径是:

createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
对应的vnode事件处理器,然后调用这个处理器从而修改vnode

//snabbdom中对事件处理做了一层包装,真实DOM的事件触发的是对vnode的操作
//主要途径是
// createListner => 返回handler作事件监听生成器 =>handler上绑定vnode =>将handler作真实DOM的事件处理器
//真实DOM事件触发后 => handler获得真实DOM的事件对象 => 将真实DOM事件对象传入handleEvent => handleEvent找到
//对应的vnode事件处理器,然后调用这个处理器从而修改vnode

//对vnode进行事件处理
function invokeHandler ( handler, vnode, event ) {
    if ( typeof handler === "function" ) {
        // call function handler
        //将事件处理器在vnode上调用
        handler.call ( vnode, event, vnode );
    }
    //存在事件绑定数据或者存在多事件处理器
    else if ( typeof handler === "object" ) {

        //说明只有一个事件处理器
        if ( typeof handler[ 0 ] === "function" ) {
            //如果绑定数据只有一个,则直接将数据用call的方式调用,提高性能
            //形如on:{click:[handler,1]}
            if ( handler.length === 2 ) {
                handler[ 0 ].call ( vnode, handler[ 1 ], event, vnode );
            }
            //如果存在多个绑定数据,则要转化为数组,用apply的方式调用,而apply性能比call差
            //形如:on:{click:[handler,1,2,3]}
            else {
                var args = handler.slice ( 1 );
                args.push ( event );
                args.push ( vnode );
                handler[ 0 ].apply ( vnode, args );
            }
        } else {
            //如果存在多个相同事件的不同处理器,则递归调用
            //如on:{click:[[handeler1,1],[handler,2]]}
            for (var i = 0; i < handler.length; i++) {
                invokeHandler ( handler[ i ] );
            }
        }
    }
}

/**
 *
 * @param event 真实dom的事件对象
 * @param vnode
 */
function handleEvent ( event, vnode ) {
    var name = event.type,
        on = vnode.data.on;

    // 如果找到对应的vnode事件处理器,则调用
    if ( on && on[ name ] ) {
        invokeHandler ( on[ name ], vnode, event );
    }
}
//事件监听器生成器,用于处理真实DOM事件
function createListener () {
    return function handler ( event ) {
        handleEvent ( event, handler.vnode );
    }
}
//更新事件监听
function updateEventListeners ( oldVnode, vnode ) {
    var oldOn = oldVnode.data.on,
        oldListener = oldVnode.listener,
        oldElm = oldVnode.elm,
        on = vnode && vnode.data.on,
        elm = vnode && vnode.elm,
        name;

    // optimization for reused immutable handlers
    //如果新旧事件监听器一样,则直接返回
    if ( oldOn === on ) {
        return;
    }

    // remove existing listeners which no longer used
    //如果新节点上没有事件监听,则将旧节点上的事件监听都删除
    if ( oldOn && oldListener ) {
        // if element changed or deleted we remove all existing listeners unconditionally
        if ( !on ) {
            for (name in oldOn) {
                // remove listener if element was changed or existing listeners removed
                oldElm.removeEventListener ( name, oldListener, false );
            }
        } else {
            //删除旧节点中新节点不存在的事件监听
            for (name in oldOn) {
                // remove listener if existing listener removed
                if ( !on[ name ] ) {
                    oldElm.removeEventListener ( name, oldListener, false );
                }
            }
        }
    }

    // add new listeners which has not already attached
    if ( on ) {
        // reuse existing listener or create new
        //如果oldvnode上已经有listener,则vnode直接复用,否则则新建事件处理器
        var listener = vnode.listener = oldVnode.listener || createListener ();
        // update vnode for listener
        //在事件处理器上绑定vnode
        listener.vnode = vnode;

        // if element changed or added we add all needed listeners unconditionally‘
        //如果oldvnode上没有事件处理器
        if ( !oldOn ) {
            for (name in on) {
                // add listener if element was changed or new listeners added
                //直接将vnode上的事件处理器添加到elm上
                elm.addEventListener ( name, listener, false );
            }
        } else {
            for (name in on) {
                // add listener if new listener added
                //否则添加oldvnode上没有的事件处理器
                if ( !oldOn[ name ] ) {
                    elm.addEventListener ( name, listener, false );
                }
            }
        }
    }
}

module.exports = {
    create: updateEventListeners,
    update: updateEventListeners,
    destroy: updateEventListeners
};

props

主要功能:

  • 从elm上删除vnode中不存在的属性

  • 更新elm上的属性

    function updateProps(oldVnode, vnode) {
      var key, cur, old, elm = vnode.elm,
          oldProps = oldVnode.data.props, props = vnode.data.props;
     //如果新旧节点都不存在属性,则直接返回
      if (!oldProps && !props) return;
      oldProps = oldProps || {};
      props = props || {};
      //删除旧节点中新节点没有的属性
      for (key in oldProps) {
        if (!props[key]) {
          delete elm[key];
        }
      }
      //更新属性
      for (key in props) {
        cur = props[key];
        old = oldProps[key];
        //如果新旧节点属性不同,且对比的属性不是value或者elm上对应属性和新属性也不同,那么就需要更新
        if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
          elm[key] = cur;
        }
      }
    }
    
    module.exports = {create: updateProps, update: updateProps};
    
    

style

主要功能如下:

  • 将elm上存在于oldvnode中但不存在于vnode中不存在的style置空

  • 如果vnode.style中的delayed与oldvnode的不同,则更新delayed的属性值,并在下一帧将elm的style设置为该值,从而实现动画过渡效果

  • 非delayed和remove的style直接更新

  • vnode被destroy时,直接将对应style更新为vnode.data.style.destory的值

  • vnode被reomve时,如果style.remove不存在,直接调用全局remove钩子进入下一个remove过程
    如果style.remove存在,那么我们就需要设置remove动画过渡效果,等到过渡效果结束之后,才调用
    下一个remove过程

    //如果存在requestAnimationFrame,则直接使用,以优化性能,否则用setTimeout
    var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout;
    var nextFrame = function(fn) { raf(function() { raf(fn); }); };
    
    //通过nextFrame来实现动画效果
    function setNextFrame(obj, prop, val) {
      nextFrame(function() { obj[prop] = val; });
    }
    
    function updateStyle(oldVnode, vnode) {
      var cur, name, elm = vnode.elm,
          oldStyle = oldVnode.data.style,
          style = vnode.data.style;
      //如果oldvnode和vnode都没有style,直接返回
      if (!oldStyle && !style) return;
      oldStyle = oldStyle || {};
      style = style || {};
      var oldHasDel = 'delayed' in oldStyle;
      //遍历oldvnode的style
      for (name in oldStyle) {
        //如果vnode中无该style,则置空
        if (!style[name]) {
          elm.style[name] = '';
        }
      }
      //如果vnode的style中有delayed且与oldvnode中的不同,则在下一帧设置delayed的参数
      for (name in style) {
        cur = style[name];
        if (name === 'delayed') {
          for (name in style.delayed) {
            cur = style.delayed[name];
            if (!oldHasDel || cur !== oldStyle.delayed[name]) {
              setNextFrame(elm.style, name, cur);
            }
          }
        }
        //如果不是delayed和remove的style,且不同于oldvnode的值,则直接设置新值
        else if (name !== 'remove' && cur !== oldStyle[name]) {
          elm.style[name] = cur;
        }
      }
    }
    
    //设置节点被destory时的style
    function applyDestroyStyle(vnode) {
      var style, name, elm = vnode.elm, s = vnode.data.style;
      if (!s || !(style = s.destroy)) return;
      for (name in style) {
        elm.style[name] = style[name];
      }
    }
    //删除效果,当我们删除一个元素时,先回调用删除过度效果,过渡完才会将节点remove
    function applyRemoveStyle(vnode, rm) {
      var s = vnode.data.style;
      //如果没有style或没有style.remove
      if (!s || !s.remove) {
        //直接调用rm,即实际上是调用全局的remove钩子
        rm();
        return;
      }
      var name, elm = vnode.elm, idx, i = 0, maxDur = 0,
          compStyle, style = s.remove, amount = 0, applied = [];
      //设置并记录remove动作后删除节点前的样式
      for (name in style) {
        applied.push(name);
        elm.style[name] = style[name];
      }
      compStyle = getComputedStyle(elm);
      //拿到所有需要过渡的属性
      var props = compStyle['transition-property'].split(', ');
      //对过渡属性计数,这里applied.length >=amount,因为有些属性是不需要过渡的
      for (; i < props.length; ++i) {
        if(applied.indexOf(props[i]) !== -1) amount++;
      }
      //当过渡效果的完成后,才remove节点,调用下一个remove过程
      elm.addEventListener('transitionend', function(ev) {
        if (ev.target === elm) --amount;
        if (amount === 0) rm();
      });
    }
    
    module.exports = {create: updateStyle, update: updateStyle, destroy: applyDestroyStyle, remove: applyRemoveStyle};
    

第五站 is

啃完modules这些大部头,总算有个比较好吃的甜品了,他主要功能就是判断是否为array类型或者原始类型

//is工具库,用于判断是否为array或者原始类型
module.exports = {
  array: Array.isArray,
  primitive: function(s) { return typeof s === 'string' || typeof s === 'number'; },
};

中途休息

看了这么多源码,估计也累了吧,毕竟一下完全理解可能有点难,不妨先休息一下,消化一下,下一章将会见到最大的boss——snabbdom本身!


steinns
168 声望30 粉丝